﻿/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Nvigorate.Common;
using Nvigorate.Common.Reflection;
using Nvigorate.Relational.Mapping.Index;
using Nvigorate.Relational.Mapping.Interface;
using Nvigorate.Relational.Provider;
using Nvigorate.Relational.Record;
using Nvigorate.Relational.State;

namespace Nvigorate.Relational.Mapping
{
    internal class BindingLambdaFactory
    {
        #region Fields

        private readonly static IMapIndex MapIndex;
        private static Func<Expression, Type, Expression> convert =
            (v, t) => Expression.Convert(v, t, TypeCaster.GetConvertMethodInfo(t));
        private static BindingDelegateIndex index = new BindingDelegateIndex();

        #endregion

        #region Constructor

        static BindingLambdaFactory()
        {
            var mapIndexFactory = new MapIndexFactory();
            MapIndex = mapIndexFactory.CreateMapIndex();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// This is the core entry point which ultimately creates the compiled binding function
        /// </summary>
        /// <typeparam name="T">The type being bound by this function</typeparam>
        /// <param name="source">The data source (connection string, file, etc.)</param>
        /// <param name="record">The 'seeder' record to base the expression tree on</param>
        /// <returns></returns>
        public static Func<IRecord, T> GetBindingLambda<T>(string source, IRecord record) where T : new()
        {
            return GetNestedBindingLambda<T>(source, record, null);
        }

        private static Func<IRecord, T> GetNestedBindingLambda<T>(string source, IRecord record, Type parentType) where T : new()
        {
            Type targetType = typeof(T);
            IBindingMap map = MapIndex[source, targetType];

            var mapExpression = Expression.Constant(map);

            ParameterExpression parameterExpression = Expression.Parameter(typeof(IRecord), "record");
            var bindings = new List<MemberBinding>();

            // Get bindings for assignments
            bindings.AddRange(
                map.GetSuppliedAssignments(record)
                    .Select(a => GetBindingExpressionForAssignment<T>(parameterExpression, a))
                    .Cast<MemberBinding>()
                );

            // Get child bindings
            bindings.AddRange(
                GetChildProperties<T>()
                    .Select(p => GetChildBindingExpression<T>(p, record, source, parameterExpression))
                    .Cast<MemberBinding>()
                );

            // Setup the object if it implements ITrackState
            //if (Reflector.ImplementsInterface(targetType, typeof(ITrackState)))
            //{
            //    bindings.Add(GetBindingExpressionForContributors<T>(parameterExpression, mapExpression));
            //    bindings.Add(Reflector.GetPropertyBindExpression(targetType, "ChangedValues", Expression.Constant(new List<Tuple<string, object>>())));
            //    bindings.Add(Reflector.GetPropertyBindExpression(targetType, "Keys", Expression.Constant(
            //                                        map
            //                                        .GetAssignmentsByPredicate(a => a.Target.PrimaryKey)
            //                                        .Select(a => a.Target.Field)
            //                                        .ToList()
            //                                     )));
            //}

            MemberInitExpression initExpression = Expression.MemberInit(
                Expression.New(targetType),
                bindings);
            LambdaExpression lambdaExpression = Expression.Lambda(typeof(Func<IRecord, T>),
                                                                  initExpression,
                                                                  parameterExpression);

            return (Func<IRecord, T>)lambdaExpression.Compile();
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// Retrieves the property info for the assignment
        /// </summary>
        /// <param name="target">The target business object type</param>
        /// <param name="assignment">The assignment to obtain the property from</param>
        /// <returns>PropertyInfo</returns>
        private static PropertyInfo GetTargetPropertyInfo(Type target, IAssignment assignment)
        {
            var property = Reflector.GetPropertyInfo(target, assignment.Target.Field, false);
            if (property == null)
                throw new ReflectorException("Type {0} does not contain a definition for property {1}",
                                             target.FullName, assignment.Target.Field);
            return property;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameterExpression"></param>
        /// <param name="assignment"></param>
        /// <returns></returns>
        private static System.Linq.Expressions.MemberAssignment GetBindingExpressionForAssignment<T>(
            ParameterExpression parameterExpression, IAssignment assignment)
        {
            Type targetType = typeof (T);
            Type propertyType = GetTargetPropertyInfo(targetType, assignment).PropertyType;

            Expression<Func<IRecord, object>> value1 = i => i.GetValueForAssignment(assignment.Source);
            Expression castExpression = convert(Expression.Invoke(value1, parameterExpression), propertyType);

            return Reflector.GetPropertyBindExpression(targetType, assignment.Target.Field, castExpression);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parameterExpression"></param>
        /// <param name="mapExpression"></param>
        /// <returns></returns>
        private static System.Linq.Expressions.MemberAssignment GetBindingExpressionForContributors<T>(
            ParameterExpression parameterExpression, Expression mapExpression)
        {
            Type targetType = typeof(T);
            Expression<Func<IRecord, IBindingMap, List<string>>> call = (r, m) => r.GetContributingSources(m);
            var caller = Expression.Invoke(call, parameterExpression, mapExpression);
            return Reflector.GetPropertyBindExpression(targetType, "Loaded", caller);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="property"></param>
        /// <param name="record"></param>
        /// <param name="source"></param>
        /// <param name="parameter"></param>
        /// <returns></returns>
        private static System.Linq.Expressions.MemberAssignment GetChildBindingExpression<T>(string property,
                                                                                             IRecord record,
                                                                                             string source,
                                                                                             ParameterExpression
                                                                                                 parameter)
            where T : new()
        {
            Type targetType = typeof (T);

            Type propertyType = Reflector.GetPropertyInfo(targetType, property, true).PropertyType;
            Type argumentType = GetEnumerableArgumentType(propertyType);
            bool isEnumerable = argumentType != null;

            var bindChildExpression = (Expression) Reflector.CallMethod(
                                                       typeof (BindingLambdaFactory),
                                                       isEnumerable ? "GetEnumerableChildBinderLambda" : "GetSingleChildBinderLambda",
                                                       new object[]
                                                           {
                                                               record, 
                                                               source, 
                                                               isEnumerable ? argumentType.Name : propertyType.Name,
                                                               parameter
                                                           },
                                                       isEnumerable ? new[] { argumentType, propertyType } : new[] { propertyType });

            Expression castExpression = convert(bindChildExpression, propertyType);
            return Reflector.GetPropertyBindExpression(targetType, property, castExpression);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="E"></typeparam>
        /// <param name="parent"></param>
        /// <param name="source"></param>
        /// <param name="relationName"></param>
        /// <param name="parameter"></param>
        /// <returns></returns>
        /// 
        private static Expression GetEnumerableChildBinderLambda<T, E>(IRecord parent, string source,
                                                                       string relationName,
                                                                       ParameterExpression parameter)
            where T : class, new()
            where E : class, IEnumerable<T>
        {
            IRecord childRecord = parent.GetRelatedSchemaRecord(relationName);

            if (childRecord != null && childRecord.FieldCount > 0)
            {
                var bind = GetBindingLambda<T>(source, childRecord);
                Expression<Func<IRecord, IEnumerable<T>>> expr =
                    i => GetInstancesFromRecords<T, E>(bind, i.GetRelatedRecords(relationName));
                return Expression.Invoke(expr, parameter);
            }

            Expression<Func<T, E>> call = t => (E) new List<T>().Select(item => item);
            return call;
        }

        private static Expression GetSingleChildBinderLambda<T>(IRecord parent, string source,
                                                                string relationName, ParameterExpression parameter)
            where T : new()
        {
            IRecord childRecord = parent.GetRelatedSchemaRecord(relationName);

            if (childRecord != null)
            {
                var bind = GetBindingLambda<T>(source, childRecord);

                Expression<Func<IRecord, T>> expr = i => GetInstanceFromRecords(bind, i.GetRelatedRecords(relationName));
                return Expression.Invoke(expr, parameter);
            }

            return Expression.New(typeof (T));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="propertyType">If property is enumerable, the type of the property, otherwise creates an IEnumerable<T> where T is the property type.</param>
        /// <returns></returns>
        private static Type GetEnumerableArgumentType(Type propertyType)
        {
            bool enumerable = propertyType.IsGenericType &&
                              Reflector.ImplementsInterface(propertyType.GetGenericTypeDefinition(),
                                                            typeof(IEnumerable));
            return enumerable ? propertyType.GetGenericArguments().First() : null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="E"></typeparam>
        /// <param name="bindingLambda"></param>
        /// <param name="records"></param>
        /// <returns></returns>
        private static IEnumerable<T> GetInstancesFromRecords<T, E>(Func<IRecord, T> bindingLambda,
                                                                    List<IRecord> records)
            where E : class, IEnumerable<T>
        {
            var enumerableType = typeof (E);
            var isIList = enumerableType.Equals(typeof (IList<T>));
            var isICollection = enumerableType.Equals(typeof (ICollection<T>));
            var canHaveState = isICollection || isIList;
            var enumerable = records.Select(bindingLambda);

            IEnumerable<T> result;
            if(canHaveState)
            {
                if(isICollection)
                    result = new StatefulCollection<T>(enumerable);
                else
                    result = new StatefulList<T>(enumerable);
            }
            else
            {
                result = enumerable;
            }

            return records.Count > 0 ? result : new List<T>().Select(t => t);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="bindingLambda"></param>
        /// <param name="records"></param>
        /// <returns></returns>
        private static T GetInstanceFromRecords<T>(Func<IRecord, T> bindingLambda, List<IRecord> records)
        {
            return records.Count > 0 ? bindingLambda(records.First()) : default(T);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        private static List<string> GetChildProperties<T>()
        {
            return Reflector.GetPropertyInfoList(typeof (T))
                .Where(p =>
                       Reflector.ImplementsInterface(p.PropertyType, typeof (ITrackState)) ||
                       Reflector.ImplementsInterface(
                           Reflector.GetGenericTypeArgument(typeof (T), p.Name) ?? typeof (object),
                           typeof (ITrackState))
                )
                .Select(p => p.Name)
                .ToList();
        }

        #endregion
    }

    public class BindingDelegateIndex
    {
        private Dictionary<Tuple<Type, string>, Delegate> _delegateLookup = new Dictionary<Tuple<Type, string>, Delegate>();
        
        private Tuple<Type, string> CreateIndexKey<T>(IRecord record)
        {
            return Tuple.Create(typeof (T), DelimitedBuilder.Construct(record.FieldNames, "|"));
        }

        public void AddDelegate<T>(IRecord record, Delegate binder)
        {
            _delegateLookup.Add(CreateIndexKey<T>(record), binder);
        }

        public Func<IRecord, T> GetDelegate<T>(IRecord record)
        {
            return _delegateLookup[CreateIndexKey<T>(record)] as Func<IRecord, T>;
        }
    }
}